﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ProjectCollectionInfo.cs" company="Scrum for Team System">
//   None
// </copyright>
// <summary>
//   The project collection.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace ScrumForTeamSystem.Setup.SfTSDeployment
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Xml;
    using System.Text.RegularExpressions;

    using Microsoft.TeamFoundation.Client;
    using Microsoft.TeamFoundation.Framework.Client;
    using Microsoft.TeamFoundation.Server;

    /// <summary>
    /// The project collection.
    /// </summary>
    public class ProjectCollectionInfo
    {


        #region Constants and Fields

        /// <summary>
        /// Reg Ex Patten
        /// </summary>
        private const string RegExPatten = @"\d+\.\d+\.\d+\.\d+";

        /// <summary>
        /// Work Item Changed Event Name
        /// </summary>
        private const string WorkItemChangedEvent = "WorkItemChangedEvent";

        /// <summary>
        /// The event service.
        /// </summary>
        private readonly IEventService eventService;

        /// <summary>
        /// The process templates.
        /// </summary>
        private readonly IProcessTemplates processTemplates;

        /// <summary>
        /// The team foundation server.
        /// </summary>
        private readonly TfsConnection teamFoundationServer;

        /// <summary>
        /// The installed tempaltes.
        /// </summary>
        private List<TemplateInfo> installedTemplates;

        /// <summary>
        /// The scubscriptions.
        /// </summary>
        private List<SubscriptionInfo> subscriptions;

        #endregion

        #region Constructors and Destructors

        /// <summary>
        /// Initializes a new instance of the <see cref="ProjectCollectionInfo"/> class.
        /// Initializes a new instance of the
        /// <see cref="ProjectCollectionInfo"/> class.
        /// </summary>
        /// <param name="tfsConfigurationServer">The team Foundation Application Instance.</param>
        /// <param name="id">The collection id.</param>
        /// <param name="name">The collection name.</param>
        /// <param name="online">The online flag.</param>
        /// <param name="tfsUrl">The tfs url.</param>
        public ProjectCollectionInfo(
            TfsConfigurationServer tfsConfigurationServer, 
            Guid id, 
            string name, 
            bool online, 
            string tfsUrl)
        {
            if (online)
            {
                this.teamFoundationServer = tfsConfigurationServer.GetTeamProjectCollection(id);
                this.eventService = this.teamFoundationServer.GetService<IEventService>();
                this.processTemplates = this.teamFoundationServer.GetService<IProcessTemplates>();
            }

            this.Id = id;
            this.Name = name;
            this.Online = online;
            this.TfsUrl = tfsUrl;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the id.
        /// </summary>
        /// <value>The collection id.</value>
        public Guid Id { get; set; }

        /// <summary>
        /// Gets or sets Name.
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether Online.
        /// </summary>
        public bool Online { get; set; }

        /// <summary>
        /// Gets or sets TfsUrl.
        /// </summary>
        public string TfsUrl { get; set; }

        #endregion

        #region Public Methods

        /// <summary>
        /// Adds the subscription.
        /// </summary>
        /// <param name="address">The address.</param>
        /// <param name="filter">The filter.</param>
        /// <returns>The subscription id.</returns>
        public int AddSubscription(string address, string filter)
        {
            var deliveryPreference = new DeliveryPreference
                {
                    Type = DeliveryType.Soap, 
                    Address = address 
                };
            
            var subsId = this.eventService.SubscribeEvent(WorkItemChangedEvent, filter, deliveryPreference);

            // Clear the subscriptions cache.
            this.subscriptions = null;

            return subsId;
        }

        /// <summary>
        /// Gets the list of installed templates.
        /// </summary>
        /// <returns>List of installed tempaltes.</returns>
        public List<TemplateInfo> GetListOfInstalledTemplates()
        { 
            if (this.installedTemplates == null)
            {
                var templates = new List<TemplateInfo>();
                if (this.Online)
                {
                    var xmlNode = this.processTemplates.GetTemplateNames();
                    var templateNodes = xmlNode.GetEnumerator();
                    var regex = new Regex(RegExPatten, RegexOptions.Singleline);
                    TemplateInfo templateInfo;
                    XmlNode currentNode;
                    string currentTemplateName, currentTemplateVersion;
                    
                    while (templateNodes.MoveNext())
                    {
                        templateInfo = new TemplateInfo();
                        currentNode = (XmlNode)templateNodes.Current;
                        currentTemplateName = currentNode.SelectSingleNode("templateName").InnerText;
                        currentTemplateVersion = regex.Match(currentTemplateName).Value;
                        templateInfo.Id = int.Parse(currentNode.SelectSingleNode("templateId").InnerText);
                        templateInfo.Name = currentTemplateName;
                        templateInfo.IsScrumTemplate = currentTemplateName.Contains("Scrum for Team System");
                        templateInfo.ScrumVersion = templateInfo.IsScrumTemplate && currentTemplateVersion.Length > 0
                                                        ? new Version(currentTemplateVersion)
                                                        : new Version();

                        templates.Add(templateInfo);
                    }
                }

                this.installedTemplates = templates;
            }

            return this.installedTemplates;
        }

        /// <summary>
        /// The list subscription.
        /// </summary>
        /// <returns>List of Configured Subscription elements</returns>
        public List<SubscriptionInfo> ListSubscription()
        {
            if (this.subscriptions == null)
            {
                var configureSubscriptions = new List<SubscriptionInfo>();
                if (this.Online)
                {
                    foreach (Subscription subscription in this.eventService.GetAllEventSubscriptions())
                    {
                        configureSubscriptions.Add(
                            new SubscriptionInfo(
                                subscription.ID,
                                subscription.ConditionString,
                                subscription.DeliveryPreference.Address,
                                subscription.DeliveryPreference.Schedule.ToString(),
                                subscription.DeliveryPreference.Type.ToString(),
                                subscription.Device,
                                subscription.EventType,
                                subscription.Subscriber,
                                subscription.Tag,
                                subscription.Tag.Contains("SfTS")));
                    }
                }

                this.subscriptions = configureSubscriptions;
            }

            return this.subscriptions;
        }

        /// <summary>
        /// The load process template.
        /// </summary>
        /// <param name="metaDataFilePath">The meta data file path.</param>
        /// <param name="zipFilePath">The zip file path.</param>
        /// <exception cref="FileNotFoundException"></exception>
        public void DeployProcessTemplate(string metaDataFilePath, string zipFilePath)
        {
            if (!this.Online)
            {
                return;
            }

            if (!File.Exists(metaDataFilePath))
            {
                throw new FileNotFoundException(
                    string.Format("Unable to find meta data file: '{0}'", metaDataFilePath));
            }

            if (!File.Exists(zipFilePath))
            {
                throw new FileNotFoundException(
                    string.Format("Unable to find template zip file: '{0}'", zipFilePath));
            }

            string name, description, metaDataFileContents;
            if (!TryGetMetaFileContents(metaDataFilePath, out name, out description, out metaDataFileContents))
            {
                throw new ArgumentException(
                    string.Format("The specified meta data file '{0}' is not valid.", metaDataFilePath));
            }

            this.processTemplates.AddUpdateTemplate(
                name, description, metaDataFileContents, "visible", zipFilePath);

            // Clear the installed template cache.
            this.installedTemplates = null;
        }

        /// <summary>
        /// Removes the template.
        /// </summary>
        /// <param name="templateId">The template id.</param>
        public void RetractProcessTemplate(int templateId)
        {
            this.processTemplates.DeleteTemplate(templateId);

            // Clear the installed template cache.
            this.installedTemplates = null;
        }

        /// <summary>
        /// The remove event subscription.
        /// </summary>
        /// <param name="subsId">
        /// The subs id.
        /// </param>
        public void RemoveEventSubscription(int subsId)
        {
            this.eventService.UnsubscribeEvent(subsId);

            // Clear the subscriptions cache.
            this.subscriptions = null;
        }

        /// <summary>
        /// Determines whether the specified template is deployed.
        /// </summary>
        /// <param name="metaDataFilePath">The meta data file path.</param>
        /// <returns>
        /// <c>true</c> if the specified template is deployed; otherwise, <c>false</c>.
        /// </returns>
        public bool IsTemplateDeployed(string metaDataFilePath)
        {
            string name, description, metaDataFileContents;
            if (!TryGetMetaFileContents(metaDataFilePath, out name, out description, out metaDataFileContents))
            {
                return false;
            }

            var templates = this.GetListOfInstalledTemplates();

            return templates.Any(t => t.Name.Equals(name));
        }

        /// <summary>
        /// Gets the deployed template id.
        /// </summary>
        /// <param name="metaDataFilePath">The meta data file path.</param>
        /// <returns><c>Id</c> of the deployed template if found; otherwise null.</returns>
        public int? GetDeployedTemplateId(string metaDataFilePath)
        {
            string name, description, metaDataFileContents;
            if (!TryGetMetaFileContents(metaDataFilePath, out name, out description, out metaDataFileContents))
            {
                return null;
            }

            var templates = this.GetListOfInstalledTemplates();

            var deployedTemplate = templates.FirstOrDefault(t => t.Name.Equals(name));

            return deployedTemplate == null ? null : (int?)deployedTemplate.Id;
        }

        /// <summary>
        /// Determines whether this instance [has event subscription].
        /// </summary>
        /// <param name="address">The address.</param>
        /// <param name="filter">The filter.</param>
        /// <returns>
        /// <c>true</c> if this instance [has event subscription]; otherwise, <c>false</c>.
        /// </returns>
        public bool HasEventSubscription(string address, string filter)
        {
            this.subscriptions = this.subscriptions ?? this.ListSubscription();

            return
                this.subscriptions.Any(
                    s => 
                    s.EventType.Equals(WorkItemChangedEvent) 
                    && IgnoreHostCompare(s.DeliveryPreferenceAddress, address));
        }

        /// <summary>
        /// Gets the evetn subscription id.
        /// </summary>
        /// <param name="address">The address.</param>
        /// <param name="filter">The filter.</param>
        /// <returns>The event subscription id.</returns>
        public int? GetEventSubscriptionId(string address, string filter)
        {
            this.subscriptions = this.subscriptions ?? this.ListSubscription();

            var subscriptionInfo = this.subscriptions.FirstOrDefault(
                s =>
                s.EventType.Equals(WorkItemChangedEvent)
                && IgnoreHostCompare(s.DeliveryPreferenceAddress, address));

            return subscriptionInfo == null ? null : (int?)subscriptionInfo.Id;
        }

        #endregion

        /// <summary>
        /// Compares two URL strings whilst ignorring the host name.
        /// </summary>
        /// <param name="addressA">The address A.</param>
        /// <param name="addressB">The address B.</param>
        /// <returns><c>True</c> if the addresses match; otherwise <c>false</c>.</returns>
        private static bool IgnoreHostCompare(string addressA, string addressB)
        {
            Func<Uri, string> stripHost = uri => string.Concat(uri.Scheme, uri.Port, uri.AbsolutePath, uri.Query);

            Uri uriA, uriB;
            if (!Uri.TryCreate(addressA, UriKind.Absolute, out uriA) || !Uri.TryCreate(addressB, UriKind.Absolute, out uriB))
            {
                return false;
            }

            return string.Compare(stripHost(uriB), stripHost(uriA), true) == 0;
        }

        /// <summary>
        /// Tries the get meta file contents.
        /// </summary>
        /// <param name="path">The meta data file path.</param>
        /// <param name="name">The template name.</param>
        /// <param name="description">The description.</param>
        /// <param name="contents">The contents.</param>
        /// <returns><c>True</c> if the specfied meta data file is loaded succesfuly; otherwise <c>false</c>.</returns>
        private static bool TryGetMetaFileContents(string path, out string name, out string description, out string contents)
        {
            name = null;
            description = null;
            contents = null;

            if (File.Exists(path))
            {
                // Load the meta data from file
                var xmlDoc = new XmlDocument();
                xmlDoc.Load(path);

                name = xmlDoc.SelectSingleNode("/metadata/name").InnerText;
                description = xmlDoc.SelectSingleNode("/metadata/description").InnerText;
                contents = File.ReadAllText(path);
            }
            else
            {
                throw new FileNotFoundException(string.Format("Unable to find meta data file: '{0}'", path));
            }

            return !string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(description) & !string.IsNullOrEmpty(contents);
        }
    }
}